Release 10.1A: OpenEdge Development:
ProDataSets
Extending the samples to GET, SAVE, MERGE, and ACCEPT changes
Now, you’ll extend
dsOrderWinUpd.wto use these methods to return changes to the database. Note that you’ll make a whole series of changes to the procedures. In some cases, one change replaces a change made earlier. We do this to show how you can use the low-level methods and attributes, and then how the higher-level methods can do a lot of the work for you, replacing the code you wrote the first time through. The versions of these procedures that are saved with the other examples represent the final state of the procedures. 4GL statements used in earlier stages of a procedure’s development are commented out so that you can examine them if you need to. However, you should build your own versions of the procedures step by step in order to learn to use all the different levels of statements and attributes available to you.
![]()
To extend the procedure:
- In
dsOrderWinUpd.w, Remove or comment out the entireFOR EACHblock with theMESSAGEstatement in theCHOOSEtrigger for BtnSave, and add these variable definitions to the top of the block:
- This series of statements in the
CHOOSEtrigger creates the change ProDataSet:
The
CREATE DATASETstatement simply allocates a structure for the ProDataSet definition and points the handlehDSChangesat that structure.The
CREATE-LIKEmethod creates the dynamic temp-tables and Data-Relations in that structure. If you provide a prefix as a second argument toCREATE-LIKE, then the temp-table names are prefixed by that string. Otherwise, they have the same names as in the original ProDataSet.The
GET-CHANGESmethod copies all the before-table rows and their after-table partners into the dynamic change ProDataSet.- Turn the
TRACKING-CHANGESflag off for thettOlinetable, before passing it to the update procedure for processing, as shown:
The
MERGE-CHANGESmethod that you will use later to merge any final field updates back into thettOlinetable the browse is displaying requires thatTRACKING-CHANGESbe false. In any case, you need to set it off at some point as part of preparing for the next set of changes the user makes.- Add a statement to run a new business logic procedure that accepts the changes and writes them back to the database, as shown:
The change ProDataSet is an
INPUT-OUTPUTparameter so that the window procedure gets back any final changes to the data, as well as any messages. TheBY-REFERENCEqualifier tells Progress to share the same ProDataSet instance whenupdateOrder.pis run locally.- Define the update procedure
updateOrder.p:
Because the procedure uses the static temp-table and ProDataSet definitions, it can receive the dynamic change ProDataSet that was passed into it as a
DATASET-HANDLEusing the staticDATASETparameter form. If this procedure is run locally within the same session as the window, as it is in this simplified example, then the ProDataSet is passed by reference so thatupdateOrder.pis actually pointing to the dynamic ProDataSet as it was created in the window procedure. If the same call is made remotely across an AppServer connection, then theBY-REFERENCEqualifier is ignored and the ProDataSet is marshaled across the network in both directions. The net result is the same, so this single call lets Progress run the procedure in the most efficient way whether or not the application is actually distributed.The update procedure needs these variable definitions. You will need the Data-Source definition for the
OrderLinetable later in the procedure. Define and attach the Data-SourcesrcOline, as shown:
Just to verify that the right data got transferred into the change ProDataSet, you can get the handle to the ProDataSet and the after-table buffer for
ttOline. For example:
You can then retrieve the corresponding before-table buffer in this way:
This is a bit convoluted, as you have to go from the after-table buffer to its temp-table handle, then to the before-table temp-table handle, and then from there to the before-table’s default buffer handle. Alternatively, you can accomplish the same thing with the
BEFORE-BUFFERorAFTER-BUFFERattribute:
Remember that the
BEFORE-TABLEandAFTER-TABLEattributes are on the temp-table handles and return a temp-table handle. TheBEFORE-BUFFERandAFTER-BUFFERattributes are on the buffer handles and return a buffer handle.You can likewise find the first row in the before-table and display the
Pricefrom both buffers to show that the procedure got the right records, as shown:
You can save the
updateOrder.pprocedure and run the window to confirm this. Select anOrder, modify the price of one of itsOrderLinesand click Save Changes:
![]()
Because there is a static definition of the ProDataSet and its temp-tables, you can access the before-table and its buffer directly by name. This code begins a
CASEstatement to process different kinds of changes:
In this example, you’ll just handle modified rows, not creates or deletes. Defining the scope of the transaction is your responsibility. There are so many different ways in which you might want to handle multiple related changes. You can accept each successful change and reject the ones that fail. You can reject the entire set of updates if anything fails. Or you can define anything in between. In this example each modified row is a separate transaction. This is appropriate if they are independent to the extent that you are not leaving the database in an invalid state if you allow some changes to be committed while returning an error status to be corrected for others.
Also, remember that as always, the default buffer name for a temp-table is the same as the temp-table name. Depending on which one you are referring to, you might need to qualify the reference with the keyword
BUFFERorTEMP-TABLE. In this case, theFOR EACHstatement always expects a buffer name, so there is no need to qualify the name to tell Progress that this is a buffer reference. TheCASEstatement, however, doesn’t know what to expect, so you have to provide an explicit reference toBUFFERttOlineBeforeso that Progress knows to look for theROW-STATEattribute on the buffer, not its temp-table. Remember, also, that the keywordROW-MODIFIEDevaluates to the integer value2, which is the actual value theROW-STATEattribute returns.For each modified row, you need to assign the changes back to the database. To demonstrate what the
SAVE-ROW-CHANGESmethod does for you, you can do the same work “by hand” in 4GL statements so that you understand all the steps.First, you need to find and lock the database record that was used to populate the changed row. In this case, the unique key is composed of the
OrderNumandLineNumfields:
In the general case, it can be difficult to assemble the proper
where-clauseto retrieve the database record. This is what theSAVE-WHERE-STRINGattribute on the Data-Source is for. You can substitute that value, which in this case is the same as the string in theFINDstatement above, starting with the keywordWHERE. In order to access theSAVE-WHERE-STRINGattribute, you must first attach the Data-Source. You did this at the top of the procedure. With that done, this statement can replace theFINDstatement in the last code block:
Don’t be confused by these two related Data-Source attributes:
SAVE-WHERE-STRINGis the where-clause needed to retrieve the right database record to match a before-table record that you’re using as the basis of an update. This is why it compares the database buffer with the before-table buffer to identify a match. TheSAVE-WHERE-STRINGattribute requires an argument, which is the index of the database buffer you’re trying to retrieve. In this example,OrderLineis the first (and only) database buffer for the Data-SourcesrcOrder.Design tip: Use the attribute values such as- By contrast,
FILL-WHERE-STRINGis the where-clause needed to retrieve the right child database records for the current parent record when doing an automatedFILL. In thewhere-clauseforttOline, for instance,FILL-WHERE-STRINGjoins the parentttOrdertemp-table to theOrderLinedatabase table.SAVE-WHERE-STRINGandFILL-WHERE-STRINGwherever possible to generate ProDataSet-specific code for you. This way your procedures will be more flexible, more reusable, and less prone to error if the underlying table definitions change.You need to compare the before-table record with what’s in the database, to make sure no-one else changed it since your procedure read it into the original ProDataSet. You can set the
ERROR-STRINGattribute for the row if there’s a conflict. For example:
If there’s no conflict with another change to the same database record, next you need to find the after-table row for this change and copy its values into the database:
It’s important to examine why we used the
BUFFER-COMPAREandBUFFER-COPYmethods on the buffer handles here rather than theBUFFER-COMPAREandBUFFER-COPYstatements. After all, we’re dealing with static buffers, so the statements would have been usable.The reason, as you should recall, is that the
Design tip: Use the dynamicBUFFER-COMPAREandBUFFER-COPYmethods have been extended to use the Data-Source field mapping list and field include list when they are used to compare a ProDataSet temp-table buffer to its Data-Source buffer. In this case the definition ofttOlineis simple enough that the static statements would have worked correctly. There are no field name changes betweenOrderLineandttOline, and no limited list of fields to include in the copy or compare. But in other cases where there is a field mapping or an include field list, the static statements would not work unless you went to the trouble of including the field mapping and include list as options on the static statement. If you use the methods instead, this is done for you.BUFFER-COPYandBUFFER-COMPAREmethods wherever possible to copy rows into and out of ProDataSet temp-tables. Even if a table definition has no field mapping or include field list, your copy and compare will continue to work without change in the future if the table definitions ever change.You need to retrieve any changes made by database triggers into the temp-table in preparation for returning it to the caller. To do this, you release the database buffer to force any triggers to fire, and then re-read the record,
NO-LOCKthis time, and buffer-copy it back into the after-table. You can use theSAVE-WHERE-STRINGattribute again to find the record, this timeNO-LOCK. For example:
You end all the procedure blocks:
- Go back to the
CHOOSE OF BtnSavetrigger in the window proceduredsOrderWinUpd.w.It receives back the modified
ttOlinerows as part of thedsOrderINPUT-OUTPUTparameter. You have to merge these final changes back into your original ProDataSet so that they show up in the user interface. TheMERGE-CHANGESmethod can do this for you, but again let’s go through the steps in 4GL code to confirm what the method will do for you.- Create a dynamic query to walk through the after-table for
OrderLinesin the change ProDataSet, as shown:
Because you used a dynamic ProDataSet to hold the changes by using the
CREATE DATASETstatement and theCREATE-LIKEmethod, all references to the ProDataSet need to be dynamic.- Add the one temp-table buffer to the query:
Design tip: You can refer to the buffer in the
GET-BUFFER-HANDLEmethod by position, as in this example, or by name (“ttOline” in this case) as you did inupdateOrder.p. If you’re writing general-purpose code that needs to be reusable for a variety of ProDataSets, then the position option is more flexible because it does not hardcode the buffer name into the procedure.One other important thing to note here is that you must not refer to the buffer name directly without getting it through its dynamic ProDataSet. That is, you can refer to the buffer name relative to the ProDataSet, like this:
But you must not just refer to the buffer name directly in this way:
The reason is that if you don’t give Progress any context for the reference to
Design tip: As these examples show, you will often need to refer to multiple different temp-tables and buffers with the same name when you are working with ProDataSets. Make sure that you properly reference dynamic references so that they point to the proper table or buffer.ttOline, it locates the static temp-table definition forttOline, which is part of the original static ProDataSetdsOrder. This is the wrong buffer in this case. You have to direct Progress to use the buffer in the dynamic change ProDataSet. This is a by-product of the fact that it is acceptable to have multiple objects with the same name within a procedure if no more than one is statically defined, but you must refer to the dynamic objects that share the name through their handles or the handles of their parents. So in this example, any unqualified reference tottOlinerefers to the static temp-table’s buffer. Any references to the dynamicttOlinemust be through a handle.If you had used the prefix argument to prepend a string to the beginning of each temp-table name, then you could safely refer to the buffer by name in an
ADD-BUFFERmethod, because the name would be unique.While we’re on the subject of buffer names, there’s one more thing you should understand about the buffer names in these dynamic ProDataSets. The before-table and its buffer in any dynamic ProDataSet are given the name
“BI”plus the after-table name (up to32characters). So, for example, the before-table forttOlinein a dynamic ProDataSet that youCREATE-LIKEdsOrderis namedBIttOline. If you had specified the prefix argument such as“chg”to theCREATE-LIKEmethod, then the before-table name would bechgBIttOline.- Prepare the dynamic query to walk through the after-table rows:, as shown
Here again the statement merits a brief discussion of the alternatives.
The
QUERY-PREPAREmethod requires a string that evaluates toFOR EACHttOLine. You could do this just by passing that literal string toQUERY-PREPAREdirectly. In the example above, the reference is more indirect, going through the buffer handle of the ProDataSet. This kind of reference is useful when you want to be able to reuse the same code for potentially different temp-table names.You might wonder why it would be acceptable to refer to
ttOlinedirectly in theQUERY-PREPAREmethod, as inhQuery:QUERY-PREPARE(“FOR EACHttOline”), when it is not acceptable to refer to this dynamic buffer directly by name in the precedingADD-BUFFERmethod, as inhQuery:ADD-BUFFER(“ttOline”). The reason is that theADD-BUFFERmethod has no context for the name. You could be adding anyttOlinebuffer to this dynamic query. This is why you need to make sure that it uses the right one by referencing it through its parent ProDataSet handle. However, having done this, theQUERY-PREPAREmethod can use the name of the buffer without a problem because it knows that this query is for the particularttOlinebuffer that was established by theADD-BUFFERmethod.- Open the after-table query and position to the first row in the table, as shown:
Instead of getting the buffer handle from the query, you could also get it from the ProDataSet reference that was used to build the query. For example:
Note that in the first instance,
GET-BUFFER-HANDLEis operating on the query, which in this case has only one buffer. This makes the buffer number argument optional. The alternative uses the same method name to extract the second buffer handle (forttOline) relative to the ProDataSet it’s a part of.- Walk through the rows in the query and locate the corresponding row in the original
ttOlinetable. Buffer-copy the final values to the original ProDataSet table.Here’s how to do this with specific code to use the key fields from
ttOlineto identify the appropriate row in the other table:
This technique has its problems, though. For one thing, it’s difficult to generalize. This
FINDstatement is very specific to theOrderLinetable and its key fields. Second, if the key field values have been assigned by the update logic (as is often the case for a newly created record), then the key values won’t even match up.For this reason Progress provides the
ORIGIN-ROWIDattribute to point directly at the corresponding row in the origin ProDataSet. This value is assigned by Progress for every modified row when you execute theGET-CHANGESmethod specifically so that you can identify the right rows in the origin ProDataSet at the time of a merge. Naturally, you have to be merging changes back into exactly the same ProDataSet instance asGET-CHANGESwas run on. Otherwise, the temp-tableRowIDvalues will not match. Progress uses theORIGIN-HANDLEattribute internally when it executes theMERGE-CHANGESmethod to verify this. You can useORIGIN-HANDLEyourself if you are doing the work ofMERGE-CHANGESin your own 4GL code and there is a need to verify that the ProDataSet you’re merging changes back into is the appropriate one.The
ORIGIN-ROWIDattribute is set for both the before-table and after-table rows in the change ProDataSet, so that you can start with either one. It holds theRowIDof the before-table row in the origin ProDataSet. If you are applying final changes back to modified or created records, then you need to get to the corresponding after-table row in the origin ProDataSet. This makes identifying the right row a two-step process. Here are the statements that can replace theFINDstatement above:
From the after-table buffer for
ttOlinein the change ProDataSet (which is the handlehBuffer), you use a dynamicFIND-BY-ROWIDto position the before-table forttOlinein the origin ProDataSet to the before-image of the same row. Then you reference this record’sAFTER-ROWIDto identify and find the after-table row inttOlinein the origin ProDataSet. This is the row you want to copy final field changes to. TheBUFFER-COPYmethod does this.This two-step process is unavoidable. First, you need to back out a delete that has failed on the server in the origin ProDataSet. To do so, you must locate the before-table row in the origin ProDataSet to delete it after you re-create the undeleted row in the after-table. This is all part of what Progress does for you when you use the
MERGE-CHANGESmethod.- Synchronize the top-level query (on
ttOrder), as shown:
This is necessary to force the relation queries to re-open and refresh the browse. Using the preprocessor
{&OPEN-QUERY-OlineBrowse}would not work because the query the browse is using is the query on the Data-Relation, not the staticttOlinequery generated by the AppBuilder.- Use the
ACCEPT-CHANGESmethod to clear all the before-table records and to accept the new values in any changed after-table records as the starting point for any further changes. Delete the dynamic change ProDataSet now that you’re done with it:
- Enable the
OrderNumfield again so the user can request anotherOrderand disable the Save Changes button until there are more changes to save. Also, turn theTRACKING-CHANGESflag back on forttOlineto capture any further changes that are made to thisOrderbefore anotherOrderis selected. For example:
Because this is the trigger for the button, you can refer to it as
SELF, and no frame qualifier is needed.- Go into the property sheet for BtnSave and make it initially disabled. It will be enabled by the code when there are changes to save.
- In the
ROW-LEAVEtrigger forOlineBrowse, enable the Save button if the row was modified, as shown::
- In the
LEAVEtrigger for fieldiOrderNum, disable the Save button when a newOrderis selected:
Here the
DOblock around theASSIGNstatement scopes the references to thedsFrame, so theIN FRAMEphrase isn’t needed.If you save this and rerun the window procedure, you can see the effect of the code that writes the changes back to the database.
- Select one or more
OrderLines, change thePrice(and alsoQtyandDiscountif you like), and choose the Save Changes button.You see the confirmation that the records were written back to the database, because the
Extended Pricefield, which is calculated by a database trigger procedure, it changed to reflect the newPrice,Qty, andDiscount. For example:
![]()
Changing the
PriceandQtyof line 2 recalculates theExtended Price, as shown:
![]()
|
Copyright © 2005 Progress Software Corporation www.progress.com Voice: (781) 280-4000 Fax: (781) 280-4095 |